fix(mobile): preserve exact mention recipients in saved drafts - #7387
fix(mobile): preserve exact mention recipients in saved drafts#7387loganj wants to merge 2 commits into
Conversation
6fd3b66 to
5696495
Compare
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
5696495 to
8a78110
Compare
Independent durable-mention readiness evidence — 2026-09-07Verified current stacked range:
2026-09-07 update: the stale composed reference described above has been retired — the |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — request changes
Reviewed base 39afd73b0adfde14164f4b10dbd089cb498312b6 through exact head 8a781108507dc4501192b0101185decc1ed6f118.
Major — saved display classification can authorize a later membership mutation
DraftMention.isAgent is persisted as device-local JSON (mobile/lib/shared/mentions/draft_mention.dart:3-24) and restored directly into MentionCandidate.isAgent (mobile/lib/features/channels/compose_bar/draft_lifecycle.dart:97-109). On send, a currently non-member candidate with that restored bit is classified into agentPubkeys (mobile/lib/features/channels/compose_bar/helpers.dart:541-551) and silently added with role=bot (helpers.dart:606-617, via _addMentionedNonMembers at lines 436-474).
That makes stale presentation metadata an authorization/provenance input. A draft can be saved while a pubkey is an eligible directory agent, then reopened after that directory or verified classification disappears. Sending still emits a durable kind 9000 bot-membership mutation instead of using the human invite / reference-only path. The relay does not repair this trust boundary: its PUT_USER policy permits an authorized channel member to assign the bot role (crates/buzz-relay/src/handlers/channel_authz.rs:119-166). This also contradicts DraftMention's own contract that display classification is not authorization.
Two independent production-widget probes reproduced the stale-provenance path at this head: persisted is_agent: true, no current member/directory-agent evidence, then reopen/send emitted kind 9000 with the target p tag and role=bot, without a human confirmation dialog. The checked-in restart/rejected-send regression does not exercise classification changing between save and send.
Required change: Preserve the exact label-to-pubkey binding, but re-derive agent status and mention eligibility at send from current authoritative evidence (current membership role and current verified directory/NIP-OA evidence). If current evidence cannot establish agent status, fail closed into the human invite/reference-only path or another explicit safe choice. Add a production-seam regression covering: eligible agent selected → draft saved → current agent provenance removed/changed → restart/reopen/send → no silent bot-role PUT_USER, with the intended fallback asserted.
Validation
- Focused mobile production-seam suites: 129 passed on a clean tree with head checked before and after as
8a781108507dc4501192b0101185decc1ed6f118. - The new checked-in widget regression
exact draft recipients survive restart and failed-send recovery: passed independently. git diff --check 39afd73b0adfde14164f4b10dbd089cb498312b6..8a781108507dc4501192b0101185decc1ed6f118: passed.- GitHub Mobile CI and required checks are green on this head.
- Supplemental full mobile suite: 2,084 passed, but its post-run SHA could not be re-attested after the disposable worktree metadata was removed, so it is not relied upon for this verdict.
Confidence gap: no native iOS simulator/device journey was observed, so keyboard, VoiceOver, and native lifecycle behavior remain unverified. This is verification outstanding, not separate author rework.
🤖
Summary
Saved drafts remembered their text but not exactly who each mention referred to: reopening a draft (or restarting the app) could re-bind a mention to the wrong same-name teammate, and a draft restored in one thread could capture text arriving for another. This PR saves the exact selected identities alongside the draft text and restores them together, so a reopened draft names the same recipients — with their agent markings — that you picked, including after a failed send.
Related issue
Testing
just mobile-check, the full mobile test suite, and full localjust ciall pass — receipts in the exact-head evidence comment.To see it: mention an agent or a namesake, leave the channel, restart the app, and return — the draft still names the exact recipients you picked.
Screenshots
Flutter production-widget test renders — not native-device screenshots or acceptance captures.
Capture provenance
Rendered by the Flutter widget engine in a
flutter testrun (production widgets, production theme; no device or simulator). Before: this PR's declared base39afd73b0adfde14164f4b10dbd089cb498312b6. After: its head8a781108507dc4501192b0101185decc1ed6f118.